// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Test that the enqueuers are not dependent upon in which order impacts are
// applied.

import 'package:compiler/src/compiler.dart';
import 'package:compiler/src/commandline_options.dart';
import 'package:compiler/src/common/elements.dart';
import 'package:compiler/src/elements/entities.dart';
import 'package:compiler/src/elements/names.dart';
import 'package:compiler/src/elements/types.dart';
import 'package:compiler/src/enqueue.dart';
import 'package:compiler/src/inferrer/typemasks/masks.dart';
import 'package:compiler/src/universe/call_structure.dart';
import 'package:compiler/src/universe/selector.dart';
import 'package:compiler/src/universe/world_builder.dart';
import 'package:compiler/src/universe/world_impact.dart';
import 'package:compiler/src/universe/use.dart';
import 'package:compiler/src/js_model/js_world.dart' show JClosedWorld;
import 'package:expect/async_helper.dart';
import 'package:expect/expect.dart';
import 'package:compiler/src/util/memory_compiler.dart';

class Test {
  final String name;
  final String code;
  final List<Impact> impacts;
  final Map<String, List<String>> expectedLiveMap;

  const Test({
    required this.name,
    required this.code,
    required this.impacts,
    required this.expectedLiveMap,
  });

  Map<String, List<String>> get expectedLiveResolutionMap {
    Map<String, List<String>> map = {};
    expectedLiveMap.forEach((String clsName, List<String> memberNames) {
      for (String memberName in memberNames) {
        if (memberName.startsWith('?')) {
          memberName = memberName.substring(1);
        }
        map.putIfAbsent(clsName, () => []).add(memberName);
      }
    });
    return map;
  }

  Map<String, List<String>> get expectedLiveCodegenMap {
    Map<String, List<String>> map = {};
    expectedLiveMap.forEach((String clsName, List<String> memberNames) {
      for (String memberName in memberNames) {
        if (memberName.startsWith('?')) {
          // Skip for codegen
          continue;
        }
        map.putIfAbsent(clsName, () => []).add(memberName);
      }
    });
    return map;
  }
}

enum ImpactKind { instantiate, invoke }

class Impact {
  final ImpactKind kind;
  final String clsName;
  final String memberName;

  const Impact.instantiate(this.clsName, [this.memberName = ''])
    : this.kind = ImpactKind.instantiate;
  const Impact.invoke(this.clsName, this.memberName)
    : this.kind = ImpactKind.invoke;

  @override
  String toString() =>
      'Impact(kind=$kind,clsName=$clsName,memberName=$memberName)';
}

const List<Test> tests = const <Test>[
  const Test(
    name: 'Instantiate class',
    code: '''
class A {
  void method() {}
}
''',
    impacts: const [
      const Impact.instantiate('A'),
      const Impact.invoke('A', 'method'),
    ],
    expectedLiveMap: const {
      'A': const ['', 'method'],
    },
  ),
  const Test(
    name: 'Instantiate subclass',
    code: '''
class A {
  void method() {}
}
class B extends A {
}
''',
    impacts: const [
      const Impact.instantiate('B'),
      const Impact.invoke('B', 'method'),
    ],
    expectedLiveMap: const {
      'A': const ['?', 'method'],
      'B': const [''],
    },
  ),
  const Test(
    name: 'Instantiate superclass/subclass',
    code: '''
class A {
  void method() {}
}
class B extends A {
}
''',
    impacts: const [
      const Impact.instantiate('A'),
      const Impact.instantiate('B'),
      const Impact.invoke('B', 'method'),
    ],
    expectedLiveMap: const {
      'A': const ['', 'method'],
      'B': const [''],
    },
  ),
];

main() {
  asyncTest(() async {
    for (Test test in tests) {
      await runTest(test);
    }
  });
}

runTest(Test test) async {
  print('====================================================================');
  print('Running test ${test.name}');
  for (List<Impact> permutation in permutations(test.impacts)) {
    print('------------------------------------------------------------------');
    print('Permutation: $permutation');
    await runTestPermutation(test, permutation);
  }
}

Iterable<List<Impact>> permutations(List<Impact> impacts) sync* {
  int length = impacts.length;
  if (length <= 1) {
    yield impacts;
  } else {
    for (int index = 0; index < length; index++) {
      Impact head = impacts[index];
      List<Impact> tail = List<Impact>.from(impacts)..removeAt(index);
      for (List<Impact> permutation in permutations(tail)) {
        yield [head]..addAll(permutation);
      }
    }
  }
}

runTestPermutation(Test test, List<Impact> impacts) async {
  Compiler compiler = compilerFor(
    memorySourceFiles: {
      'main.dart':
          '''
${test.code}
main() {}
''',
    },
    options: [Flags.disableInlining],
    entryPoint: Uri.parse('memory:main.dart'),
  );

  void checkInvariant(
    Enqueuer enqueuer,
    ElementEnvironment elementEnvironment,
  ) {
    for (MemberEntity member in enqueuer.processedEntities) {
      Expect.isTrue(
        member == elementEnvironment.mainFunction ||
            member.library != elementEnvironment.mainLibrary,
        "Unexpected member $member in ${enqueuer}.",
      );
    }
  }

  void instantiate(
    Enqueuer enqueuer,
    ElementEnvironment elementEnvironment,
    String name,
  ) {
    ClassEntity cls = elementEnvironment.lookupClass(
      elementEnvironment.mainLibrary!,
      name,
    )!;
    ConstructorEntity constructor = elementEnvironment.lookupConstructor(
      cls,
      '',
    )!;
    InterfaceType type = elementEnvironment.getRawType(cls);
    WorldImpact impact = WorldImpactBuilderImpl()
      ..registerStaticUse(
        new StaticUse.typedConstructorInvoke(
          constructor,
          constructor.parameterStructure.callStructure,
          type,
          null,
        ),
      );
    enqueuer.applyImpact(impact);
  }

  void invoke(
    Enqueuer enqueuer,
    ElementEnvironment elementEnvironment,
    String className,
    String methodName,
    Object Function(ClassEntity cls) createConstraint,
  ) {
    ClassEntity cls = elementEnvironment.lookupClass(
      elementEnvironment.mainLibrary!,
      className,
    )!;
    Selector selector = Selector.call(
      Name(methodName, elementEnvironment.mainLibrary!.canonicalUri),
      CallStructure.noArgs,
    );
    WorldImpact impact = WorldImpactBuilderImpl()
      ..registerDynamicUse(
        DynamicUse(selector, createConstraint(cls), const <DartType>[]),
      );
    enqueuer.applyImpact(impact);
  }

  void applyImpact(
    Enqueuer enqueuer,
    ElementEnvironment elementEnvironment,
    Impact impact,
    Object Function(ClassEntity cls) createConstraint,
  ) {
    switch (impact.kind) {
      case ImpactKind.instantiate:
        instantiate(enqueuer, elementEnvironment, impact.clsName);
        break;
      case ImpactKind.invoke:
        invoke(
          enqueuer,
          elementEnvironment,
          impact.clsName,
          impact.memberName,
          createConstraint,
        );
        break;
    }
  }

  void checkLiveMembers(
    Enqueuer enqueuer,
    ElementEnvironment elementEnvironment,
    Map<String, List<String>> expectedLiveMap,
  ) {
    Map<String, List<String>> actualLiveMap = {};
    for (MemberEntity member in enqueuer.processedEntities) {
      if (member != elementEnvironment.mainFunction &&
          member.library == elementEnvironment.mainLibrary) {
        actualLiveMap
            .putIfAbsent(member.enclosingClass!.name, () => [])
            .add(member.name!);
      }
    }

    Expect.setEquals(
      expectedLiveMap.keys,
      actualLiveMap.keys,
      "Unexpected live classes in $enqueuer\n "
      "Expected: ${expectedLiveMap.keys}\n "
      "Actual  : ${actualLiveMap.keys}",
    );
    expectedLiveMap.forEach((String clsName, List<String> expectedMembers) {
      List<String> actualMembers = actualLiveMap[clsName]!;
      Expect.setEquals(
        expectedMembers,
        actualMembers,
        "Unexpected live members for $clsName in $enqueuer\n "
        "Expected: $expectedMembers\n "
        "Actual  : $actualMembers",
      );
    });
  }

  compiler.onResolutionQueueEmptyForTesting = () {
    Enqueuer enqueuer = compiler.resolutionEnqueuerForTesting;
    ElementEnvironment elementEnvironment =
        compiler.frontendStrategy.elementEnvironment;
    checkInvariant(enqueuer, elementEnvironment);

    Object createConstraint(ClassEntity cls) {
      return defaultReceiverClass(
        compiler.frontendStrategy.commonElements,
        compiler.frontendStrategy.elementMap.nativeBasicData,
        cls,
      );
    }

    for (Impact impact in impacts) {
      applyImpact(enqueuer, elementEnvironment, impact, createConstraint);
    }
  };
  compiler.onCodegenQueueEmptyForTesting = () {
    Enqueuer enqueuer = compiler.codegenEnqueuerForTesting;
    JClosedWorld closedWorld = compiler.backendClosedWorldForTesting!;
    ElementEnvironment elementEnvironment =
        compiler.backendClosedWorldForTesting!.elementEnvironment;
    final domain = closedWorld.abstractValueDomain as CommonMasks;
    checkInvariant(enqueuer, elementEnvironment);

    Object createConstraint(ClassEntity cls) {
      return TypeMask.subtype(cls, domain);
    }

    for (Impact impact in impacts) {
      applyImpact(enqueuer, elementEnvironment, impact, createConstraint);
    }
  };

  await compiler.run();

  checkLiveMembers(
    compiler.resolutionEnqueuerForTesting,
    compiler.frontendStrategy.elementEnvironment,
    test.expectedLiveResolutionMap,
  );

  checkLiveMembers(
    compiler.codegenEnqueuerForTesting,
    compiler.backendClosedWorldForTesting!.elementEnvironment,
    test.expectedLiveCodegenMap,
  );
}
